package com.zdp.controller;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author sesshomaru
 * @date 2021/10/5 15:13
 */
@Slf4j
@RestController
public class TestController {

    @Autowired
    private DiscoveryClient discoveryClient;


    /**
     * 服务发现，证明内容中心永远都可以找到用户中心
     * @return
     */
    @GetMapping("test2")
    public List<ServiceInstance> setDiscoveryClient() {
        // 查询指定服务的所有信息
        // 不管是consul/eureka/zookeeper都可以调用DiscoveryClient的接口api获取服务信息
        return this.discoveryClient.getInstances("user-center");
    }

    /**
     * 注意
     * 记住这里最大的区别在于，一旦触发了熔断器，则不到熔断时间到，即使请求是正常的也会进入熔断的处理逻辑里面
     *
     * 使用sentinel的api保护接口
     * 更优雅的方式是通过注解的方式保护接口
     *
     * 配置了降级后，发生异常达到设定的降级阈值，则会直接走到catch (BlockException e)里面，在设定的熔断时长中一直都会走到catch (BlockException e)中
     * 只有等熔断时长到了，才会去请求被保护的方法，如果成功则关闭熔断，如果还是失败就会继续熔断等待下一次熔断时间到再次调用被保护的方法
     *
     * 在熔断器的处理上sentinel跟hystrix还是有一点区别的
     * hystrix需要设定一个时间窗口时间和半开时间(当熔断开启以后，经过多少秒再进入半开状态  注意这里的进入半开状态的时间一定要小于时间窗口时间)
     * 半开状态时就会请求被保护的方法，如果请求正常则会关闭断路器
     *
     * sentinel 则是到达降级的阈值后触发熔断，此时再次请求目标方法不会再直接调用而是直接走降级流程了，只有等本次熔断时间过去后就会再次调用
     * 目标方法，如果请求正常则会关闭断路器
     *
     * 可见两者有再半开上有区别，hystrix半开时间需要自行指定而sentinel的半开时间就是本次熔断时间到达之后。这里的半开时间就是在熔断的情况下
     * 尝试再次请求被保护方法的时间
     *
     * @param a
     * @return
     */
    @GetMapping("/test-sentinel-api")
    public String testSentinelApi(@RequestParam(required = false) String a) {
        String resourceName = "test-sentinel-api";
        // 配置一下针对来源
        // ContextUtil.enter(resourceName, "test-wfw");
        Entry entry = null;
        try {
            // 定义一个sentinel保护的资源,  名称是test-sentinel-api
            entry = SphU.entry(resourceName);
            // TODO 业务begin
            // 模拟业务异常
            if (StringUtils.isBlank(a)) {
                throw new IllegalArgumentException("a不能为空");
            }
            // 被保护的业务逻辑
            // TODO 业务end
            return a;
        } catch (BlockException e) {
            // 资源访问阻止，被限流或被降级
            // 在此处进行相应的处理操作
            log.warn("限流，或者降级了");
            return "限流，或者降级了";

        } catch (Exception ex) {
            // 统计IllegalArgumentException，异常次数，异常占比......
            // 若需要配置降级规则，需要通过这种方式记录业务异常
            Tracer.traceEntry(ex, entry);
            log.warn("调用了目标方法发生了参数非法异常");
            return "参数非法";
        }
        finally {
            // 务必保证 exit，务必保证每个 entry 与 exit 配对
            if (entry != null) {
                entry.exit();
            }
            //ContextUtil.exit();
        }
    }

    /**
     * 使用sentinel注解保护资源
     * @param id
     * @return
     */
    @GetMapping("/test-sentinel-annotation")
    @SentinelResource(value = "test-sentinel-annotation",
            blockHandler = "blockHandlerTestSentinelAnnotation",
            fallback = "helloFallback"
    )
    public String testSentinelAnnotation(@RequestParam(required = false) String id) {
        // 模拟被保护的资源发生异常
        if (StringUtils.isBlank(id)) {
            throw new IllegalArgumentException("发生业务异常,a不能为空");
        }
        return String.format("Hello at %s", id);
    }

    // Block 异常处理函数，参数最后多一个 BlockException，其余与原函数一致.
    // 若 blockHandler 和 fallback 都进行了配置，则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑
    // 专门用来处理限流、降级
    public String blockHandlerTestSentinelAnnotation(String id, BlockException ex){
        ex.printStackTrace();
        return "Oops, error occurred at " + id;
    }

    // fallback 函数名称，可选项，用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常（除了 exceptionsToIgnore 里面排除掉的异常类型）进行处理
    // 如何@SentinelResource配置了Fallback就可以用来处理业务异常
    public String helloFallback(String id, Throwable te) {
        te.printStackTrace();
        return String.format("Halooooo %s", id);
    }

}
